/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts.common.model.account;

import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.util.CommonDateUtils;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.contacts.resources.R;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

public abstract class BaseAccountType extends AccountType {

  public static final StringInflater ORGANIZATION_BODY_INFLATER =
      new StringInflater() {
        @Override
        public CharSequence inflateUsing(Context context, ContentValues values) {
          final CharSequence companyValue =
              values.containsKey(Organization.COMPANY)
                  ? values.getAsString(Organization.COMPANY)
                  : null;
          final CharSequence titleValue =
              values.containsKey(Organization.TITLE)
                  ? values.getAsString(Organization.TITLE)
                  : null;

          if (companyValue != null && titleValue != null) {
            return companyValue + ": " + titleValue;
          } else if (companyValue == null) {
            return titleValue;
          } else {
            return companyValue;
          }
        }
      };
  protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
  protected static final int FLAGS_EMAIL =
      EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
  protected static final int FLAGS_PERSON_NAME =
      EditorInfo.TYPE_CLASS_TEXT
          | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
          | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
  protected static final int FLAGS_PHONETIC =
      EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
  protected static final int FLAGS_GENERIC_NAME =
      EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
  protected static final int FLAGS_NOTE =
      EditorInfo.TYPE_CLASS_TEXT
          | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
          | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
  protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT;
  protected static final int FLAGS_WEBSITE =
      EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI;
  protected static final int FLAGS_POSTAL =
      EditorInfo.TYPE_CLASS_TEXT
          | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
          | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
          | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
  protected static final int FLAGS_SIP_ADDRESS =
      EditorInfo.TYPE_CLASS_TEXT
          | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same
  // basic format as email addresses
  protected static final int FLAGS_RELATION =
      EditorInfo.TYPE_CLASS_TEXT
          | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
          | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;

  // Specify the maximum number of lines that can be used to display various field types.  If no
  // value is specified for a particular type, we use the default value from {@link DataKind}.
  protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10;
  protected static final int MAX_LINES_FOR_GROUP = 10;
  protected static final int MAX_LINES_FOR_NOTE = 100;

  public BaseAccountType() {
    this.accountType = null;
    this.dataSet = null;
    this.titleRes = R.string.account_phone;
    this.iconRes = R.mipmap.ic_contacts_launcher;
  }

  protected static EditType buildPhoneType(int type) {
    return new EditType(type, Phone.getTypeLabelResource(type));
  }

  protected static EditType buildEmailType(int type) {
    return new EditType(type, Email.getTypeLabelResource(type));
  }

  protected static EditType buildPostalType(int type) {
    return new EditType(type, StructuredPostal.getTypeLabelResource(type));
  }

  protected static EditType buildImType(int type) {
    return new EditType(type, Im.getProtocolLabelResource(type));
  }

  protected static EditType buildEventType(int type, boolean yearOptional) {
    return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
  }

  protected static EditType buildRelationType(int type) {
    return new EditType(type, Relation.getTypeLabelResource(type));
  }

  // Utility methods to keep code shorter.
  private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
    return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
  }

  private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
    return attrs.getAttributeIntValue(null, attribute, defaultValue);
  }

  private static String getAttr(AttributeSet attrs, String attribute) {
    return attrs.getAttributeValue(null, attribute);
  }

  protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true));
    kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
    kind.actionBody = new SimpleInflater(Nickname.NAME);
    kind.typeOverallMax = 1;

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(
        new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
    kind.fieldList.add(
        new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
            .setLongForm(true));
    kind.fieldList.add(
        new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
            .setLongForm(true));
    kind.fieldList.add(
        new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
            .setLongForm(true));
    kind.fieldList.add(
        new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
            .setLongForm(true));
    kind.fieldList.add(
        new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
            .setLongForm(true));
    kind.fieldList.add(
        new EditField(
            StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
    kind.fieldList.add(
        new EditField(
            StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
    kind.fieldList.add(
        new EditField(
            StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));

    return kind;
  }

  protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
                R.string.nameLabelsGroup,
                Weight.NONE,
                true));
    kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
    kind.actionBody = new SimpleInflater(Nickname.NAME);
    kind.typeOverallMax = 1;

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(
        new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
            .setShortForm(true));

    boolean displayOrderPrimary =
        context.getResources().getBoolean(R.bool.config_editor_field_order_primary);

    if (!displayOrderPrimary) {
      kind.fieldList.add(
          new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
              .setLongForm(true));
    } else {
      kind.fieldList.add(
          new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
              .setLongForm(true));
      kind.fieldList.add(
          new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
              .setLongForm(true));
    }

    return kind;
  }

  protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
                R.string.name_phonetic,
                Weight.NONE,
                true));
    kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
    kind.actionBody = new SimpleInflater(Nickname.NAME);
    kind.typeOverallMax = 1;

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(
        new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
            .setShortForm(true));
    kind.fieldList.add(
        new EditField(
                StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)
            .setLongForm(true));
    kind.fieldList.add(
        new EditField(
                StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)
            .setLongForm(true));
    kind.fieldList.add(
        new EditField(
                StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
            .setLongForm(true));

    return kind;
  }

  protected DataKind addDataKindNickname(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                Nickname.CONTENT_ITEM_TYPE, R.string.nicknameLabelsGroup, Weight.NICKNAME, true));
    kind.typeOverallMax = 1;
    kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
    kind.actionBody = new SimpleInflater(Nickname.NAME);
    kind.defaultValues = new ContentValues();
    kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(
        new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));

    return kind;
  }

  protected DataKind addDataKindPhone(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, Weight.PHONE, true));
    kind.iconAltRes = R.drawable.quantum_ic_message_vd_theme_24;
    kind.iconAltDescriptionRes = R.string.sms;
    kind.actionHeader = new PhoneActionInflater();
    kind.actionAltHeader = new PhoneActionAltInflater();
    kind.actionBody = new SimpleInflater(Phone.NUMBER);
    kind.typeColumn = Phone.TYPE;
    kind.typeList = new ArrayList<>();
    kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
    kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
    kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
    kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
    kind.typeList.add(
        buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
    kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true));
    kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true));

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));

    return kind;
  }

  protected DataKind addDataKindEmail(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, Weight.EMAIL, true));
    kind.actionHeader = new EmailActionInflater();
    kind.actionBody = new SimpleInflater(Email.DATA);
    kind.typeColumn = Email.TYPE;
    kind.typeList = new ArrayList<>();
    kind.typeList.add(buildEmailType(Email.TYPE_HOME));
    kind.typeList.add(buildEmailType(Email.TYPE_WORK));
    kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
    kind.typeList.add(buildEmailType(Email.TYPE_MOBILE));
    kind.typeList.add(
        buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));

    return kind;
  }

  protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                StructuredPostal.CONTENT_ITEM_TYPE,
                R.string.postalLabelsGroup,
                Weight.STRUCTURED_POSTAL,
                true));
    kind.actionHeader = new PostalActionInflater();
    kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
    kind.typeColumn = StructuredPostal.TYPE;
    kind.typeList = new ArrayList<>();
    kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
    kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK));
    kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER));
    kind.typeList.add(
        buildPostalType(StructuredPostal.TYPE_CUSTOM)
            .setSecondary(true)
            .setCustomColumn(StructuredPostal.LABEL));

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(
        new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));

    kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;

    return kind;
  }

  protected DataKind addDataKindIm(Context context) throws DefinitionException {
    DataKind kind =
        addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, Weight.IM, true));
    kind.actionHeader = new ImActionInflater();
    kind.actionBody = new SimpleInflater(Im.DATA);

    // NOTE: even though a traditional "type" exists, for editing
    // purposes we're using the protocol to pick labels

    kind.defaultValues = new ContentValues();
    kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);

    kind.typeColumn = Im.PROTOCOL;
    kind.typeList = new ArrayList<>();
    kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
    kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
    kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
    kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
    kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
    kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
    kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
    kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
    kind.typeList.add(
        buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));

    return kind;
  }

  protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                Organization.CONTENT_ITEM_TYPE,
                R.string.organizationLabelsGroup,
                Weight.ORGANIZATION,
                true));
    kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup);
    kind.actionBody = ORGANIZATION_BODY_INFLATER;
    kind.typeOverallMax = 1;

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(
        new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
    kind.fieldList.add(
        new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));

    return kind;
  }

  protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
    DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, Weight.NONE, true));
    kind.typeOverallMax = 1;
    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
    return kind;
  }

  protected DataKind addDataKindNote(Context context) throws DefinitionException {
    DataKind kind =
        addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, Weight.NOTE, true));
    kind.typeOverallMax = 1;
    kind.actionHeader = new SimpleInflater(R.string.label_notes);
    kind.actionBody = new SimpleInflater(Note.NOTE);
    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));

    kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;

    return kind;
  }

  protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                Website.CONTENT_ITEM_TYPE, R.string.websiteLabelsGroup, Weight.WEBSITE, true));
    kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
    kind.actionBody = new SimpleInflater(Website.URL);
    kind.defaultValues = new ContentValues();
    kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);

    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));

    return kind;
  }

  protected DataKind addDataKindSipAddress(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                SipAddress.CONTENT_ITEM_TYPE,
                R.string.label_sip_address,
                Weight.SIP_ADDRESS,
                true));

    kind.actionHeader = new SimpleInflater(R.string.label_sip_address);
    kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS);
    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(
        new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));
    kind.typeOverallMax = 1;

    return kind;
  }

  protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException {
    DataKind kind =
        addKind(
            new DataKind(
                GroupMembership.CONTENT_ITEM_TYPE,
                R.string.groupsLabel,
                Weight.GROUP_MEMBERSHIP,
                true));

    kind.typeOverallMax = 1;
    kind.fieldList = new ArrayList<>();
    kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));

    kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;

    return kind;
  }

  @Override
  public boolean isGroupMembershipEditable() {
    return false;
  }

  /** Parses the content of the EditSchema tag in contacts.xml. */
  protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
      throws XmlPullParserException, IOException, DefinitionException {

    final int outerDepth = parser.getDepth();
    int type;
    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
        && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
      final int depth = parser.getDepth();
      if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
        continue; // Not direct child tag
      }

      final String tag = parser.getName();

      if (Tag.DATA_KIND.equals(tag)) {
        for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
          addKind(kind);
        }
      } else {
        LogUtil.i("BaseAccountType.parseEditSchema", "Skipping unknown tag " + tag);
      }
    }
  }

  private interface Tag {

    String DATA_KIND = "DataKind";
    String TYPE = "Type";
  }

  private interface Attr {

    String MAX_OCCURRENCE = "maxOccurs";
    String DATE_WITH_TIME = "dateWithTime";
    String YEAR_OPTIONAL = "yearOptional";
    String KIND = "kind";
    String TYPE = "type";
  }

  protected interface Weight {

    int NONE = -1;
    int PHONE = 10;
    int EMAIL = 15;
    int STRUCTURED_POSTAL = 25;
    int NICKNAME = 111;
    int EVENT = 120;
    int ORGANIZATION = 125;
    int NOTE = 130;
    int IM = 140;
    int SIP_ADDRESS = 145;
    int GROUP_MEMBERSHIP = 150;
    int WEBSITE = 160;
    int RELATIONSHIP = 999;
  }

  /**
   * Simple inflater that assumes a string resource has a "%s" that will be filled from the given
   * column.
   */
  public static class SimpleInflater implements StringInflater {

    private final int mStringRes;
    private final String mColumnName;

    public SimpleInflater(int stringRes) {
      this(stringRes, null);
    }

    public SimpleInflater(String columnName) {
      this(-1, columnName);
    }

    public SimpleInflater(int stringRes, String columnName) {
      mStringRes = stringRes;
      mColumnName = columnName;
    }

    @Override
    public CharSequence inflateUsing(Context context, ContentValues values) {
      final boolean validColumn = values.containsKey(mColumnName);
      final boolean validString = mStringRes > 0;

      final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
      final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null;

      if (validString && validColumn) {
        return String.format(stringValue.toString(), columnValue);
      } else if (validString) {
        return stringValue;
      } else if (validColumn) {
        return columnValue;
      } else {
        return null;
      }
    }

    @Override
    public String toString() {
      return this.getClass().getSimpleName()
          + " mStringRes="
          + mStringRes
          + " mColumnName"
          + mColumnName;
    }

    public String getColumnNameForTest() {
      return mColumnName;
    }
  }

  public abstract static class CommonInflater implements StringInflater {

    protected abstract int getTypeLabelResource(Integer type);

    protected boolean isCustom(Integer type) {
      return type == BaseTypes.TYPE_CUSTOM;
    }

    protected String getTypeColumn() {
      return Phone.TYPE;
    }

    protected String getLabelColumn() {
      return Phone.LABEL;
    }

    protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) {
      final int labelRes = getTypeLabelResource(type);
      if (type == null) {
        return res.getText(labelRes);
      } else if (isCustom(type)) {
        return res.getString(labelRes, label == null ? "" : label);
      } else {
        return res.getText(labelRes);
      }
    }

    @Override
    public CharSequence inflateUsing(Context context, ContentValues values) {
      final Integer type = values.getAsInteger(getTypeColumn());
      final String label = values.getAsString(getLabelColumn());
      return getTypeLabel(context.getResources(), type, label);
    }

    @Override
    public String toString() {
      return this.getClass().getSimpleName();
    }
  }

  public static class PhoneActionInflater extends CommonInflater {

    @Override
    protected boolean isCustom(Integer type) {
      return ContactDisplayUtils.isCustomPhoneType(type);
    }

    @Override
    protected int getTypeLabelResource(Integer type) {
      return ContactDisplayUtils.getPhoneLabelResourceId(type);
    }
  }

  public static class PhoneActionAltInflater extends CommonInflater {

    @Override
    protected boolean isCustom(Integer type) {
      return ContactDisplayUtils.isCustomPhoneType(type);
    }

    @Override
    protected int getTypeLabelResource(Integer type) {
      return ContactDisplayUtils.getSmsLabelResourceId(type);
    }
  }

  public static class EmailActionInflater extends CommonInflater {

    @Override
    protected int getTypeLabelResource(Integer type) {
      if (type == null) {
        return R.string.email;
      }
      switch (type) {
        case Email.TYPE_HOME:
          return R.string.email_home;
        case Email.TYPE_WORK:
          return R.string.email_work;
        case Email.TYPE_OTHER:
          return R.string.email_other;
        case Email.TYPE_MOBILE:
          return R.string.email_mobile;
        default:
          return R.string.email_custom;
      }
    }
  }

  public static class EventActionInflater extends CommonInflater {

    @Override
    protected int getTypeLabelResource(Integer type) {
      return Event.getTypeResource(type);
    }
  }

  public static class RelationActionInflater extends CommonInflater {

    @Override
    protected int getTypeLabelResource(Integer type) {
      return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type);
    }
  }

  public static class PostalActionInflater extends CommonInflater {

    @Override
    protected int getTypeLabelResource(Integer type) {
      if (type == null) {
        return R.string.map_other;
      }
      switch (type) {
        case StructuredPostal.TYPE_HOME:
          return R.string.map_home;
        case StructuredPostal.TYPE_WORK:
          return R.string.map_work;
        case StructuredPostal.TYPE_OTHER:
          return R.string.map_other;
        default:
          return R.string.map_custom;
      }
    }
  }

  public static class ImActionInflater extends CommonInflater {

    @Override
    protected String getTypeColumn() {
      return Im.PROTOCOL;
    }

    @Override
    protected String getLabelColumn() {
      return Im.CUSTOM_PROTOCOL;
    }

    @Override
    protected int getTypeLabelResource(Integer type) {
      if (type == null) {
        return R.string.chat;
      }
      switch (type) {
        case Im.PROTOCOL_AIM:
          return R.string.chat_aim;
        case Im.PROTOCOL_MSN:
          return R.string.chat_msn;
        case Im.PROTOCOL_YAHOO:
          return R.string.chat_yahoo;
        case Im.PROTOCOL_SKYPE:
          return R.string.chat_skype;
        case Im.PROTOCOL_QQ:
          return R.string.chat_qq;
        case Im.PROTOCOL_GOOGLE_TALK:
          return R.string.chat_gtalk;
        case Im.PROTOCOL_ICQ:
          return R.string.chat_icq;
        case Im.PROTOCOL_JABBER:
          return R.string.chat_jabber;
        case Im.PROTOCOL_NETMEETING:
          return R.string.chat;
        default:
          return R.string.chat;
      }
    }
  }

  // TODO Extract it to its own class, and move all KindBuilders to it as well.
  private static class KindParser {

    public static final KindParser INSTANCE = new KindParser();

    private final Map<String, KindBuilder> mBuilders = new ArrayMap<>();

    private KindParser() {
      addBuilder(new NameKindBuilder());
      addBuilder(new NicknameKindBuilder());
      addBuilder(new PhoneKindBuilder());
      addBuilder(new EmailKindBuilder());
      addBuilder(new StructuredPostalKindBuilder());
      addBuilder(new ImKindBuilder());
      addBuilder(new OrganizationKindBuilder());
      addBuilder(new PhotoKindBuilder());
      addBuilder(new NoteKindBuilder());
      addBuilder(new WebsiteKindBuilder());
      addBuilder(new SipAddressKindBuilder());
      addBuilder(new GroupMembershipKindBuilder());
      addBuilder(new EventKindBuilder());
      addBuilder(new RelationshipKindBuilder());
    }

    private void addBuilder(KindBuilder builder) {
      mBuilders.put(builder.getTagName(), builder);
    }

    /**
     * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns {@link
     * DataKind}s. (Usually just one, but there are three for the "name" kind.)
     *
     * <p>This method returns a list, because we need to add 3 kinds for the name data kind.
     * (structured, display and phonetic)
     */
    public List<DataKind> parseDataKindTag(
        Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final String kind = getAttr(attrs, Attr.KIND);
      final KindBuilder builder = mBuilders.get(kind);
      if (builder != null) {
        return builder.parseDataKind(context, parser, attrs);
      } else {
        throw new DefinitionException("Undefined data kind '" + kind + "'");
      }
    }
  }

  private abstract static class KindBuilder {

    public abstract String getTagName();

    /** DataKind tag parser specific to each kind. Subclasses must implement it. */
    public abstract List<DataKind> parseDataKind(
        Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException;

    /** Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind tag. */
    protected final DataKind newDataKind(
        Context context,
        XmlPullParser parser,
        AttributeSet attrs,
        boolean isPseudo,
        String mimeType,
        String typeColumn,
        int titleRes,
        int weight,
        StringInflater actionHeader,
        StringInflater actionBody)
        throws DefinitionException, XmlPullParserException, IOException {

      LogUtil.d("BaseAccountType.newDataKind", "Adding DataKind: " + mimeType);

      final DataKind kind = new DataKind(mimeType, titleRes, weight, true);
      kind.typeColumn = typeColumn;
      kind.actionHeader = actionHeader;
      kind.actionBody = actionBody;
      kind.fieldList = new ArrayList<>();

      // Get more information from the tag...
      // A pseudo data kind doesn't have corresponding tag the XML, so we skip this.
      if (!isPseudo) {
        kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);

        // Process "Type" tags.
        // If a kind has the type column, contacts.xml must have at least one type
        // definition.  Otherwise, it mustn't have a type definition.
        if (kind.typeColumn != null) {
          // Parse and add types.
          kind.typeList = new ArrayList<>();
          parseTypes(parser, attrs, kind, true);
          if (kind.typeList.size() == 0) {
            throw new DefinitionException("Kind " + kind.mimeType + " must have at least one type");
          }
        } else {
          // Make sure it has no types.
          parseTypes(parser, attrs, kind, false /* can't have types */);
        }
      }

      return kind;
    }

    /**
     * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds them to
     * the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type, so throws
     * {@link DefinitionException}.
     */
    private void parseTypes(
        XmlPullParser parser,
        AttributeSet attrs,
        DataKind kind,
        boolean canHaveTypes)
        throws DefinitionException, XmlPullParserException, IOException {
      final int outerDepth = parser.getDepth();
      int type;
      while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
          && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
        final int depth = parser.getDepth();
        if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
          continue; // Not direct child tag
        }

        final String tag = parser.getName();
        if (Tag.TYPE.equals(tag)) {
          if (canHaveTypes) {
            kind.typeList.add(parseTypeTag(attrs, kind));
          } else {
            throw new DefinitionException("Kind " + kind.mimeType + " can't have types");
          }
        } else {
          throw new DefinitionException("Unknown tag: " + tag);
        }
      }
    }

    /**
     * Parses a single Type element and returns an {@link EditType} built from it. Uses {@link
     * #buildEditTypeForTypeTag} defined in subclasses to actually build an {@link EditType}.
     */
    private EditType parseTypeTag(AttributeSet attrs, DataKind kind) throws DefinitionException {

      final String typeName = getAttr(attrs, Attr.TYPE);

      final EditType et = buildEditTypeForTypeTag(attrs, typeName);
      if (et == null) {
        throw new DefinitionException(
            "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
      }
      et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);

      return et;
    }

    /**
     * Returns an {@link EditType} for the given "type". Subclasses may optionally use the
     * attributes in the tag to set optional values. (e.g. "yearOptional" for the event kind)
     */
    protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
      return null;
    }

    protected final void throwIfList(DataKind kind) throws DefinitionException {
      if (kind.typeOverallMax != 1) {
        throw new DefinitionException("Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
      }
    }
  }

  /** DataKind parser for Name. (structured, display, phonetic) */
  private static class NameKindBuilder extends KindBuilder {

    private static void checkAttributeTrue(boolean value, String attrName)
        throws DefinitionException {
      if (!value) {
        throw new DefinitionException(attrName + " must be true");
      }
    }

    @Override
    public String getTagName() {
      return "name";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {

      // Build 3 data kinds:
      // - StructuredName.CONTENT_ITEM_TYPE
      // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
      // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME

      final boolean displayOrderPrimary =
          context.getResources().getBoolean(R.bool.config_editor_field_order_primary);

      final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false);
      final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false);
      final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false);
      final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false);
      final boolean supportsPhoneticFamilyName =
          getAttr(attrs, "supportsPhoneticFamilyName", false);
      final boolean supportsPhoneticMiddleName =
          getAttr(attrs, "supportsPhoneticMiddleName", false);
      final boolean supportsPhoneticGivenName = getAttr(attrs, "supportsPhoneticGivenName", false);

      // For now, every things must be supported.
      checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
      checkAttributeTrue(supportsPrefix, "supportsPrefix");
      checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
      checkAttributeTrue(supportsSuffix, "supportsSuffix");
      checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
      checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
      checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");

      final List<DataKind> kinds = new ArrayList<>();

      // Structured name
      final DataKind ks =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              StructuredName.CONTENT_ITEM_TYPE,
              null,
              R.string.nameLabelsGroup,
              Weight.NONE,
              new SimpleInflater(R.string.nameLabelsGroup),
              new SimpleInflater(Nickname.NAME));

      throwIfList(ks);
      kinds.add(ks);

      // Note about setLongForm/setShortForm below.
      // We need to set this only when the type supports display name. (=supportsDisplayName)
      // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
      // "optional".

      ks.fieldList.add(
          new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
      ks.fieldList.add(
          new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
              .setLongForm(true));
      ks.fieldList.add(
          new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
              .setLongForm(true));
      ks.fieldList.add(
          new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
              .setLongForm(true));
      ks.fieldList.add(
          new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
              .setLongForm(true));
      ks.fieldList.add(
          new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
              .setLongForm(true));
      ks.fieldList.add(
          new EditField(
              StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
      ks.fieldList.add(
          new EditField(
              StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
      ks.fieldList.add(
          new EditField(
              StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));

      // Display name
      final DataKind kd =
          newDataKind(
              context,
              parser,
              attrs,
              true,
              DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
              null,
              R.string.nameLabelsGroup,
              Weight.NONE,
              new SimpleInflater(R.string.nameLabelsGroup),
              new SimpleInflater(Nickname.NAME));
      kd.typeOverallMax = 1;
      kinds.add(kd);

      kd.fieldList.add(
          new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
              .setShortForm(true));

      if (!displayOrderPrimary) {
        kd.fieldList.add(
            new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
                .setLongForm(true));
      } else {
        kd.fieldList.add(
            new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
                .setLongForm(true));
        kd.fieldList.add(
            new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
                .setLongForm(true));
      }

      // Phonetic name
      final DataKind kp =
          newDataKind(
              context,
              parser,
              attrs,
              true,
              DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
              null,
              R.string.name_phonetic,
              Weight.NONE,
              new SimpleInflater(R.string.nameLabelsGroup),
              new SimpleInflater(Nickname.NAME));
      kp.typeOverallMax = 1;
      kinds.add(kp);

      // We may want to change the order depending on displayOrderPrimary too.
      kp.fieldList.add(
          new EditField(
                  DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
              .setShortForm(true));
      kp.fieldList.add(
          new EditField(
                  StructuredName.PHONETIC_FAMILY_NAME,
                  R.string.name_phonetic_family,
                  FLAGS_PHONETIC)
              .setLongForm(true));
      kp.fieldList.add(
          new EditField(
                  StructuredName.PHONETIC_MIDDLE_NAME,
                  R.string.name_phonetic_middle,
                  FLAGS_PHONETIC)
              .setLongForm(true));
      kp.fieldList.add(
          new EditField(
                  StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
              .setLongForm(true));
      return kinds;
    }
  }

  private static class NicknameKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "nickname";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Nickname.CONTENT_ITEM_TYPE,
              null,
              R.string.nicknameLabelsGroup,
              Weight.NICKNAME,
              new SimpleInflater(R.string.nicknameLabelsGroup),
              new SimpleInflater(Nickname.NAME));

      kind.fieldList.add(
          new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));

      kind.defaultValues = new ContentValues();
      kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);

      throwIfList(kind);
      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }
  }

  private static class PhoneKindBuilder extends KindBuilder {

    /** Just to avoid line-wrapping... */
    protected static EditType build(int type, boolean secondary) {
      return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
    }

    @Override
    public String getTagName() {
      return "phone";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Phone.CONTENT_ITEM_TYPE,
              Phone.TYPE,
              R.string.phoneLabelsGroup,
              Weight.PHONE,
              new PhoneActionInflater(),
              new SimpleInflater(Phone.NUMBER));

      kind.iconAltRes = R.drawable.quantum_ic_message_vd_theme_24;
      kind.iconAltDescriptionRes = R.string.sms;
      kind.actionAltHeader = new PhoneActionAltInflater();

      kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }

    @Override
    protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
      if ("home".equals(type)) {
        return build(Phone.TYPE_HOME, false);
      }
      if ("mobile".equals(type)) {
        return build(Phone.TYPE_MOBILE, false);
      }
      if ("work".equals(type)) {
        return build(Phone.TYPE_WORK, false);
      }
      if ("fax_work".equals(type)) {
        return build(Phone.TYPE_FAX_WORK, true);
      }
      if ("fax_home".equals(type)) {
        return build(Phone.TYPE_FAX_HOME, true);
      }
      if ("pager".equals(type)) {
        return build(Phone.TYPE_PAGER, true);
      }
      if ("other".equals(type)) {
        return build(Phone.TYPE_OTHER, false);
      }
      if ("callback".equals(type)) {
        return build(Phone.TYPE_CALLBACK, true);
      }
      if ("car".equals(type)) {
        return build(Phone.TYPE_CAR, true);
      }
      if ("company_main".equals(type)) {
        return build(Phone.TYPE_COMPANY_MAIN, true);
      }
      if ("isdn".equals(type)) {
        return build(Phone.TYPE_ISDN, true);
      }
      if ("main".equals(type)) {
        return build(Phone.TYPE_MAIN, true);
      }
      if ("other_fax".equals(type)) {
        return build(Phone.TYPE_OTHER_FAX, true);
      }
      if ("radio".equals(type)) {
        return build(Phone.TYPE_RADIO, true);
      }
      if ("telex".equals(type)) {
        return build(Phone.TYPE_TELEX, true);
      }
      if ("tty_tdd".equals(type)) {
        return build(Phone.TYPE_TTY_TDD, true);
      }
      if ("work_mobile".equals(type)) {
        return build(Phone.TYPE_WORK_MOBILE, true);
      }
      if ("work_pager".equals(type)) {
        return build(Phone.TYPE_WORK_PAGER, true);
      }

      // Note "assistant" used to be a custom column for the fallback type, but not anymore.
      if ("assistant".equals(type)) {
        return build(Phone.TYPE_ASSISTANT, true);
      }
      if ("mms".equals(type)) {
        return build(Phone.TYPE_MMS, true);
      }
      if ("custom".equals(type)) {
        return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
      }
      return null;
    }
  }

  private static class EmailKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "email";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Email.CONTENT_ITEM_TYPE,
              Email.TYPE,
              R.string.emailLabelsGroup,
              Weight.EMAIL,
              new EmailActionInflater(),
              new SimpleInflater(Email.DATA));
      kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }

    @Override
    protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
      // EditType is mutable, so we need to create a new instance every time.
      if ("home".equals(type)) {
        return buildEmailType(Email.TYPE_HOME);
      }
      if ("work".equals(type)) {
        return buildEmailType(Email.TYPE_WORK);
      }
      if ("other".equals(type)) {
        return buildEmailType(Email.TYPE_OTHER);
      }
      if ("mobile".equals(type)) {
        return buildEmailType(Email.TYPE_MOBILE);
      }
      if ("custom".equals(type)) {
        return buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL);
      }
      return null;
    }
  }

  private static class StructuredPostalKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "postal";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              StructuredPostal.CONTENT_ITEM_TYPE,
              StructuredPostal.TYPE,
              R.string.postalLabelsGroup,
              Weight.STRUCTURED_POSTAL,
              new PostalActionInflater(),
              new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));

      if (getAttr(attrs, "needsStructured", false)) {
        if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
          // Japanese order
          kind.fieldList.add(
              new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
                  .setOptional(true));
          kind.fieldList.add(
              new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
          kind.fieldList.add(
              new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
          kind.fieldList.add(
              new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
          kind.fieldList.add(
              new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
        } else {
          // Generic order
          kind.fieldList.add(
              new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
          kind.fieldList.add(
              new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
          kind.fieldList.add(
              new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
          kind.fieldList.add(
              new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
          kind.fieldList.add(
              new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
                  .setOptional(true));
        }
      } else {
        kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
        kind.fieldList.add(
            new EditField(
                StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));
      }

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }

    @Override
    protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
      // EditType is mutable, so we need to create a new instance every time.
      if ("home".equals(type)) {
        return buildPostalType(StructuredPostal.TYPE_HOME);
      }
      if ("work".equals(type)) {
        return buildPostalType(StructuredPostal.TYPE_WORK);
      }
      if ("other".equals(type)) {
        return buildPostalType(StructuredPostal.TYPE_OTHER);
      }
      if ("custom".equals(type)) {
        return buildPostalType(StructuredPostal.TYPE_CUSTOM)
            .setSecondary(true)
            .setCustomColumn(Email.LABEL);
      }
      return null;
    }
  }

  private static class ImKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "im";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {

      // IM is special:
      // - It uses "protocol" as the custom label field
      // - Its TYPE is fixed to TYPE_OTHER

      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Im.CONTENT_ITEM_TYPE,
              Im.PROTOCOL,
              R.string.imLabelsGroup,
              Weight.IM,
              new ImActionInflater(),
              new SimpleInflater(Im.DATA) // header / action
              );
      kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));

      kind.defaultValues = new ContentValues();
      kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }

    @Override
    protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
      if ("aim".equals(type)) {
        return buildImType(Im.PROTOCOL_AIM);
      }
      if ("msn".equals(type)) {
        return buildImType(Im.PROTOCOL_MSN);
      }
      if ("yahoo".equals(type)) {
        return buildImType(Im.PROTOCOL_YAHOO);
      }
      if ("skype".equals(type)) {
        return buildImType(Im.PROTOCOL_SKYPE);
      }
      if ("qq".equals(type)) {
        return buildImType(Im.PROTOCOL_QQ);
      }
      if ("google_talk".equals(type)) {
        return buildImType(Im.PROTOCOL_GOOGLE_TALK);
      }
      if ("icq".equals(type)) {
        return buildImType(Im.PROTOCOL_ICQ);
      }
      if ("jabber".equals(type)) {
        return buildImType(Im.PROTOCOL_JABBER);
      }
      if ("custom".equals(type)) {
        return buildImType(Im.PROTOCOL_CUSTOM)
            .setSecondary(true)
            .setCustomColumn(Im.CUSTOM_PROTOCOL);
      }
      return null;
    }
  }

  private static class OrganizationKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "organization";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Organization.CONTENT_ITEM_TYPE,
              null,
              R.string.organizationLabelsGroup,
              Weight.ORGANIZATION,
              new SimpleInflater(R.string.organizationLabelsGroup),
              ORGANIZATION_BODY_INFLATER);

      kind.fieldList.add(
          new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
      kind.fieldList.add(
          new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));

      throwIfList(kind);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }
  }

  private static class PhotoKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "photo";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Photo.CONTENT_ITEM_TYPE,
              null /* no type */,
              Weight.NONE,
              -1,
              null,
              null // no header, no body
              );

      kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));

      throwIfList(kind);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }
  }

  private static class NoteKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "note";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Note.CONTENT_ITEM_TYPE,
              null,
              R.string.label_notes,
              Weight.NOTE,
              new SimpleInflater(R.string.label_notes),
              new SimpleInflater(Note.NOTE));

      kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
      kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;

      throwIfList(kind);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }
  }

  private static class WebsiteKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "website";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Website.CONTENT_ITEM_TYPE,
              null,
              R.string.websiteLabelsGroup,
              Weight.WEBSITE,
              new SimpleInflater(R.string.websiteLabelsGroup),
              new SimpleInflater(Website.URL));

      kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));

      kind.defaultValues = new ContentValues();
      kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }
  }

  private static class SipAddressKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "sip_address";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              SipAddress.CONTENT_ITEM_TYPE,
              null,
              R.string.label_sip_address,
              Weight.SIP_ADDRESS,
              new SimpleInflater(R.string.label_sip_address),
              new SimpleInflater(SipAddress.SIP_ADDRESS));

      kind.fieldList.add(
          new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));

      throwIfList(kind);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }
  }

  private static class GroupMembershipKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "group_membership";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              GroupMembership.CONTENT_ITEM_TYPE,
              null,
              R.string.groupsLabel,
              Weight.GROUP_MEMBERSHIP,
              null,
              null);

      kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
      kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;

      throwIfList(kind);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }
  }

  /**
   * Event DataKind parser.
   *
   * <p>Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
   */
  private static class EventKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "event";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Event.CONTENT_ITEM_TYPE,
              Event.TYPE,
              R.string.eventLabelsGroup,
              Weight.EVENT,
              new EventActionInflater(),
              new SimpleInflater(Event.START_DATE));

      kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));

      if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
        kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
        kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
      } else {
        kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
        kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
      }

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }

    @Override
    protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
      final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);

      if ("birthday".equals(type)) {
        return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
      }
      if ("anniversary".equals(type)) {
        return buildEventType(Event.TYPE_ANNIVERSARY, yo);
      }
      if ("other".equals(type)) {
        return buildEventType(Event.TYPE_OTHER, yo);
      }
      if ("custom".equals(type)) {
        return buildEventType(Event.TYPE_CUSTOM, yo)
            .setSecondary(true)
            .setCustomColumn(Event.LABEL);
      }
      return null;
    }
  }

  /**
   * Relationship DataKind parser.
   *
   * <p>Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
   * now.
   */
  private static class RelationshipKindBuilder extends KindBuilder {

    @Override
    public String getTagName() {
      return "relationship";
    }

    @Override
    public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
        throws DefinitionException, XmlPullParserException, IOException {
      final DataKind kind =
          newDataKind(
              context,
              parser,
              attrs,
              false,
              Relation.CONTENT_ITEM_TYPE,
              Relation.TYPE,
              R.string.relationLabelsGroup,
              Weight.RELATIONSHIP,
              new RelationActionInflater(),
              new SimpleInflater(Relation.NAME));

      kind.fieldList.add(
          new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));

      kind.defaultValues = new ContentValues();
      kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);

      List<DataKind> result = new ArrayList<>();
      result.add(kind);
      return result;
    }

    @Override
    protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
      // EditType is mutable, so we need to create a new instance every time.
      if ("assistant".equals(type)) {
        return buildRelationType(Relation.TYPE_ASSISTANT);
      }
      if ("brother".equals(type)) {
        return buildRelationType(Relation.TYPE_BROTHER);
      }
      if ("child".equals(type)) {
        return buildRelationType(Relation.TYPE_CHILD);
      }
      if ("domestic_partner".equals(type)) {
        return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
      }
      if ("father".equals(type)) {
        return buildRelationType(Relation.TYPE_FATHER);
      }
      if ("friend".equals(type)) {
        return buildRelationType(Relation.TYPE_FRIEND);
      }
      if ("manager".equals(type)) {
        return buildRelationType(Relation.TYPE_MANAGER);
      }
      if ("mother".equals(type)) {
        return buildRelationType(Relation.TYPE_MOTHER);
      }
      if ("parent".equals(type)) {
        return buildRelationType(Relation.TYPE_PARENT);
      }
      if ("partner".equals(type)) {
        return buildRelationType(Relation.TYPE_PARTNER);
      }
      if ("referred_by".equals(type)) {
        return buildRelationType(Relation.TYPE_REFERRED_BY);
      }
      if ("relative".equals(type)) {
        return buildRelationType(Relation.TYPE_RELATIVE);
      }
      if ("sister".equals(type)) {
        return buildRelationType(Relation.TYPE_SISTER);
      }
      if ("spouse".equals(type)) {
        return buildRelationType(Relation.TYPE_SPOUSE);
      }
      if ("custom".equals(type)) {
        return buildRelationType(Relation.TYPE_CUSTOM)
            .setSecondary(true)
            .setCustomColumn(Relation.LABEL);
      }
      return null;
    }
  }
}
